/*
 * Copyright 2008 Lukas Vlcek
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License. 
 */

package org.radio.lastfm.service;

import com.googlecode.ascrblr.api.radio.RadioService;
import com.googlecode.ascrblr.api.radio.StationUrl;
import com.googlecode.ascrblr.api.radio.MetaData;
import com.googlecode.ascrblr.api.util.ServiceException;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.SwingWorker;
import javazoom.jl.player.Player;
import org.openide.util.Exceptions;
import org.openide.util.Lookup;
import org.openide.util.lookup.AbstractLookup;
import org.openide.util.lookup.InstanceContent;
import org.openide.util.lookup.Lookups;
import org.radio.lastfm.control.action.ControlButton;
import org.radio.lastfm.control.action.Play;
import org.radio.lastfm.control.action.Skip;
import org.radio.lastfm.control.action.Stop;
import org.radio.lastfm.control.action.Tunein;
import org.radio.lastfm.utils.CallbackAction;
import org.radio.lastfm.utils.PlayerLookup;
import org.radio.lastfm.utils.ThrowableDelivery;

public class PlayerService implements Lookup.Provider {

    private static final Logger logger = Logger.getLogger(PlayerService.class.getName());
    
    public enum SearchBy { globaltags, artist };

    private static PlayerLookup lookup;
    private static InstanceContent dynamicContent = new InstanceContent();
    private static Lookup dynamicLookup;
    
    private static SwingWorker playerSwingWorker = null;
    private static JLPlayerWrapper jlPlayer = null;
    
    private static volatile RadioService radioService = null;
    private static StationUrl stationUrl = null;
    private static MetaData metaData = null;
    private static String radioTagTitle = null;
    
    private static SwingWorker updaterSwingWorker = null;
    // 60 seconds (by default) before metadata change is checked
    private static final Integer DEFAUL_INTERVAL = 60;
    // then repeat the check every 10 seconds
    private static final Integer REFRESH_INTERVAL = 10;
    
    // initialization of lookup
    static {
        // TODO See some "Double Checking" tutorial in order to make this code robust!
        
        lookup = new PlayerLookup();
        dynamicLookup = new AbstractLookup(dynamicContent);
        lookup.setMyLookups(dynamicLookup);
        
        jlPlayer = new JLPlayerWrapper();
    }
    
    // Singleton service class
    private PlayerService() {};
    
    private static RadioService getRadio() {
        // TODO See some "Double Checking" tutorial in order to make this code robust!
        if (radioService == null) {
            radioService = new RadioService();
        }
        return radioService;
    }
    
    public static StationUrl getNewStationUrl(String type, String value) {
        value = value.replaceAll("\\s+", "%2520");
        StationUrl station = new StationUrl("lastfm://" + type + "/" + value);
        return station;
    }
    
    public static void play(final ThrowableDelivery delivery) {

        if (jlPlayer.getJlPlayer() == null && playerSwingWorker == null && stationUrl != null) {
            
            playerSwingWorker = new SwingWorker() {

                private Throwable t = null;
                
                @Override
                protected Object doInBackground() throws Exception {
                    
                    try {

                        BufferedInputStream mp3Stream = getRadio().getMp3Stream();

                        int buff = 0;
                        // 8000 is empirical magic constant
                        // I will have to find a better way how to learn
                        // if the Radio buffered enough data for the playback to start and
                        // for the metadata to be loaded. Unfortunately, the Radio library
                        // does not seem to provide anything useful now...
                        while (mp3Stream.available() < 8000) {
                            if (logger.isLoggable(Level.INFO) && buff < mp3Stream.available()) {
                                buff = mp3Stream.available();
                                logger.log(Level.INFO, "Buffering... " + buff);
                            }
                            try {
                                Thread.sleep(50);
                            } catch (InterruptedException e) {
                                return null;
                            }
                        }   

                        MetaData md = getRadio().getMetaData();
                        Integer interval = getTrackDuration(md);
                        updateMetadata(md);

                        jlPlayer.setJlPlayer(new Player(mp3Stream));

                        scheduleMetadataUpdater(interval);

                        setControl(Skip.getInstance(), Stop.getInstance(), Tunein.getInstance());
                        
                        jlPlayer.getJlPlayer().play();
                        
                    } catch (Exception e) {
                        t = e;
                    } finally {
                        // empty now...
                    }
                    return null;
                }
                
                @Override
                protected void done() {
                    if (t != null) {
                        if (delivery != null) {delivery.deliver(t);}
                    }
                }
                
            };
            
            playerSwingWorker.execute();

        }
    }

    /**
     * 
     * @param action is called in the end of this method. Can be null.
     */
    public static void stop(final CallbackAction action) {
        
        if (updaterSwingWorker != null) {
            logger.log(Level.FINE, updaterSwingWorker.hashCode() + " updater is set to null.");
            updaterSwingWorker.cancel(true);
            updaterSwingWorker = null;
        }
        
        if (playerSwingWorker != null && jlPlayer.getJlPlayer() != null) {
                        
            jlPlayer.getJlPlayer().close();
            jlPlayer.setJlPlayer(null);
            updateMetadata(null);
            
            // Worker thread should be done after player is closed
            // If it is not then give it next chance...
            if (!playerSwingWorker.isDone()) {
                playerSwingWorker.cancel(true);
            }
                       
            playerSwingWorker = null;
            logger.log(Level.FINE, "Radio streaming stopped.");
            
        } else if (playerSwingWorker != null && jlPlayer.getJlPlayer() == null) {
            
            logger.log(Level.FINE, "Radio probably hanged in buffering...");
            try {
                // TODO Why am I updating metadata here?
                updateMetadata(getRadio().getMetaData());
            } catch (Exception ex) {
                Exceptions.printStackTrace(ex);
            } finally {
                
                boolean cancelled = playerSwingWorker.cancel(true);
                logger.log(Level.FINE, "Operation cancelled? " + cancelled);
                playerSwingWorker = null;
                
            }
        }
        
        if (action != null) {
            action.execute();
        }

    }
    
    public static void setControl(ControlButton... controls) {
        List<ControlButton> ctrls = Arrays.asList(controls);
        Lookup[] lookups = new Lookup[ctrls.size()+1];
        // Interface can not contain static method definition
        // see http://www.jguru.com/faq/view.jsp?EID=88225
        for (int i = 0; i < ctrls.size(); i++) {
            if (ctrls.get(i).getClass().equals(Play.class)) {
                lookups[i] = Lookups.singleton(Play.getInstance());
            } else
            if (ctrls.get(i).getClass().equals(Stop.class)) {
                lookups[i] = Lookups.singleton(Stop.getInstance());
            } else
            if (ctrls.get(i).getClass().equals(Skip.class)) {
                lookups[i] = Lookups.singleton(Skip.getInstance());
            } else
            if (ctrls.get(i).getClass().equals(Tunein.class)) {
                lookups[i] = Lookups.singleton(Tunein.getInstance());
            }
        }
        lookups[ctrls.size()] = dynamicLookup;
        lookup.setMyLookups(lookups);
    }
    
    /**
     * Does not work...? using stop() and play() instead.
     */
    /*
    public static void skip() {
        if (playerSwingWorker != null && radioService != null) {
            try {
                logger.log(Level.INFO, "Skipping song.");
                radioService.executeCommand(Command.SKIP);
            } catch (IOException ex) {
                Exceptions.printStackTrace(ex);
            } catch (ServiceException ex) {
                Exceptions.printStackTrace(ex);
            }
        }
    }
     */
    
    /**
     * This SwingWorker is to the the following:
     * 1) wait for the song to be almost finished
     * 2) check new metadata in short intervals
     * 3) if new metadata found then schedule new updater...
     */
    private static void scheduleMetadataUpdater(final int delay) {
        if (updaterSwingWorker == null) {
            
            updaterSwingWorker = new SwingWorker() {

                @Override
                protected Object doInBackground() throws Exception {
                    try {
                        // Let's make the duration shorter by 10%
                        Thread.sleep( (long) ((delay - (delay * 0.1)) * 1000));
                        // repeat refresh only 100 times ... then give up.
                        for (int i = 0; i < 100; i++) {
                            MetaData md = getRadio().getMetaData();
                            if (!md.toString().equals(metaData.toString())) {
                                updateMetadata(md);
                                return null;
                            }
                            Thread.sleep(REFRESH_INTERVAL * 1000);
                        }
                    } catch (InterruptedException e) {
                        // Exceptions.printStackTrace(e);
                        // song was stopped
                    }
                    return null;
                }
                
                @Override
                protected void done() {
                    boolean cancelled = false;
                    if (updaterSwingWorker != null) {
                        logger.log(Level.FINE, updaterSwingWorker.hashCode() + " is done? " + updaterSwingWorker.isDone());
                        logger.log(Level.FINE, updaterSwingWorker.hashCode() + " is cancelled? " + updaterSwingWorker.isCancelled());
                        
                        cancelled = updaterSwingWorker.isCancelled();
                        
                        updaterSwingWorker.cancel(true);
                        updaterSwingWorker = null;
                    }
                    if (!cancelled) {
                        // regular track end, need to schedule new updater
                        scheduleMetadataUpdater(getTrackDuration(metaData));
                    }
                }
                
            };
            logger.log(Level.INFO, updaterSwingWorker.hashCode() + " scheduler " +
                    " will start approximately in " + delay + " seconds");
            updaterSwingWorker.execute();
        } else {
            logger.log(Level.INFO, updaterSwingWorker.hashCode() + " scheduler exists, new scheduler was not set!");
        }
    }
    
    private static Integer getTrackDuration(MetaData md) {
        Integer interval = DEFAUL_INTERVAL; // 60 sec
        if (md != null) {
            String duration = md.getTrack().getTrackDuration(); // why is this String?! Huh!
            try {
                interval = Integer.parseInt(duration);
            } catch (Exception e) {
                // ignore for now... use default value.
            }
        }
        return interval;
    }
    
    private static void updateMetadata(MetaData md) {
        
        if (dynamicContent != null) {
            
            // Did we receive metadata for a new track?
            // MetaData class does not override equals method
            //(I should add an issue for ascrblr into issues list)
            if (metaData != null && md != null &&
                    metaData.toString().equals(md.toString())) {
                // do nothing...
            } else {            
                // Remove old instance of MetaData from lookup
                if (metaData != null) {
                    logger.log(Level.FINEST, "Removing old MetaData from lookup");
                    dynamicContent.remove(metaData);
                }
                if (md != null) {
                    metaData = md;
                    // ... and add a new one
                    logger.log(Level.FINEST, "Adding new MetaData into lookup");
                    dynamicContent.add(metaData);
                }
            }
        }
        //logger.log(Level.FINEST, "Song metaData = " + metaData);
    }
    
    /**
     * 
     * @return MetaData MetaData for actual track
     */     
    public static MetaData getActualMetaData() {
        MetaData metadata = null;
        if (getRadio() != null) {
            try {                        
                metadata = getRadio().getMetaData();
            } catch (IOException ex) {
                Exceptions.printStackTrace(ex);
            } catch (ServiceException ex) {
                Exceptions.printStackTrace(ex);
            }
        }
        return metadata;
    }
    
    /**
     * If there are new track metadata available then
     * update its instance in lookup. This will be reflected
     * by all relevant GUI components.
     */
    public static void checkNewMetaData() {
        updateMetadata(getActualMetaData());
    }
    
    /**
     * Returns number of seconds since the moment the player has been started.
     * This does not generally return number of seconds from the beginning of
     * the track (with the exception for the very first track).
     * 
     * @return int number of seconds
     */
    public static int getActualPlayerPosition() {
        int result = 0;
        if (jlPlayer.getJlPlayer() != null) {
            result = jlPlayer.getJlPlayer().getPosition();
        }
        return result;
    }
    
    public static StationUrl getStationUrl() {
        return stationUrl;
    }

    public static void setRadioTagTitle(String title) {
        radioTagTitle = title;
    }
    
    public static String getRadioTagTitle() {
        return radioTagTitle;
    }
    
    public static void tuneInStation(final StationUrl stationUrl,
            final String username, final String userpswd,
            final ThrowableDelivery delivery,
            final CallbackAction successAction,
            final CallbackAction failureAction) {
        
        PlayerService.stationUrl = stationUrl;
        
        new SwingWorker() {

                private Throwable t = null;
                
                @Override
                protected Object doInBackground() throws Exception {
                    
                    try {
                        if (username != null && userpswd != null) {
                            getRadio().setCredentials(username, userpswd);
                        }
                        getRadio().tuneInStation(stationUrl);
                        setRadioTagTitle(getRadio().getXSPF().getTitle());
                    } catch (Throwable e) {
                        t = e;
                    }
                    return null;
                }
                
                @Override
                protected void done() {
                    if (t != null) {
                        failureAction.execute();
                        if (delivery != null) { delivery.deliver(t); }
                    } else {
                        successAction.execute();
                    }
                }
                
        }.execute();
        
    }

    public static Lookup getStaticLookup() {
        return lookup;
    }

    public Lookup getLookup() {
        // implementation of this method is forced by implemented interface
        // but we are using the static method instead...
        return lookup;
    }
    
};
